using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;

namespace Backfire
{
  /// <summary>
  /// For each Backfire POST that goes to the server, a PostProcessor instance must be created.
  /// When given the desired changes and the LoadCSS/SaveCSS delegates, it handles everything.
  /// </summary>
  class PostProcessor
  {
    private string uri = "";
    private string selector = "";
    private string action = "";
    private string declaration = "";
    private string value = "";
    private Dictionary<string, string> uriContents = new Dictionary<string, string>();

    /// <summary>
    /// Processes the posted changes by going through them sequentially. It builds up a
    /// dictionary of uri/css-contents key/value pairs. When needed it performs a call
    /// to cssLoader in order to retrieve the original CSS. When all changes are processed,
    /// it will make the appropriate calls to the cssSaver.
    /// </summary>
    /// <param name="changes">The changes posted by the BackFire.js library</param>
    /// <param name="cssLoader">Delegate to resolve css given a uri</param>
    /// <param name="cssSaver">Delegate to save the final CSS with</param>
    /// <returns>A success or fail message.</returns>
    public PostbackMessage Process(string changes, LoadCSS cssLoader, SaveCSS cssSaver)
    {
      PostbackMessage result = PostbackMessage.SaveSuccessful;
      string[] actions = changes.Split(new char[] { '\n' }, StringSplitOptions.RemoveEmptyEntries);
      foreach (string line in actions)
      {
        action = line.Substring(0, 1);
        string contents = line.Substring(2).Trim('\r', '\n');        
        string[] pairs = contents.Split(new char[] { ':' }, StringSplitOptions.RemoveEmptyEntries);
        switch (action)
        {
          case "u": // uri
            uri = contents;
            // if we've already opened this css file, get the contents from memory
            if (!uriContents.ContainsKey(uri))
            {              
              string css = cssLoader(uri);
              uriContents.Add(uri, css);
            }
            break;
          case "s": // selector
            selector = contents;
            break;
          case "c": // change
          case "r": // remove
          case "a": // add
            declaration = pairs[0];
            value = pairs[1];
            string selectorPattern = selector.Replace(".", "\\.");
            selectorPattern = Regex.Replace(selectorPattern, " ", @"\s+");
            // pattern: start of file or } required, followed by 0 or more (enter, space, comment), then the selector, followed by 0 or more (enter, space, comment) and a {
            uriContents[uri] = Regex.Replace(uriContents[uri], @"((^|\})([\s\n\r]|(/\*.*?\*/))*" + selectorPattern + @"([\s\n\r]|(/\*.*?\*/))*\{)([^\}]*?)([\n\r\s]*\})", new MatchEvaluator(replaceCssRule), RegexOptions.Singleline);
            break;
        }
      }

      // now call the save delegate for all changes per uri
      foreach (string uri in uriContents.Keys)
      {
        string contents = uriContents[uri];
        bool success = cssSaver(uri, contents);
        if (!success)
          result = PostbackMessage.SaveFailed;          
      }

      return result;
    }

    /// <summary>
    /// Regex MatchEvaluator to replace a css rule with its new contents.
    /// Used in changing, adding or deleting css declarations.
    /// </summary>
    /// <param name="m">The regular expression match</param>
    /// <returns>The new css rule in its entirety</returns>
    private string replaceCssRule(Match m)
    {
      string selectorText = m.Groups[1].Value;
      string declarationText = m.Groups[7].Value;
      string closingAccolade = m.Groups[8].Value;
      string result = selectorText + declarationText + closingAccolade;

      // split the entire declaration body in separate declaration/value lines
      string[] decls = declarationText.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries);

      // build up the new declaration body
      string newDecl = "";
      string indentation = "  "; // 2 spaces

      // iterate over all separate decl/value items
      foreach (string decl in decls)
      {
        // split name from value
        string[] declPairs = decl.Split(new char[] { ':' }, StringSplitOptions.RemoveEmptyEntries);
        if (declPairs.Length > 1)
        {
          string curDeclaration = declPairs[0];
          // get the correct indentation based off existing declarations
          indentation = Regex.Replace(curDeclaration, @"^(\s*).*$", "$1");
          // curValue is everything from the colon. Not declPairs[1] as the value could contain more colons, like in a url
          string curValue = decl.Substring(decl.IndexOf(":") + 1);

          // if this is not the declaration we're looking for, use the original decl and move on
          string declarationToCheck = Regex.Replace(curDeclaration, "/\\*.*?\\*/", "", RegexOptions.IgnoreCase).ToLower().Trim();
          if (declarationToCheck != declaration.ToLower().Trim())
          {
            newDecl += decl + ";";
            continue;
          }
          // otherwise, only act upon a "c" (change) action.
          else if (action == "c")
          {
            string newValue = Regex.Replace(curValue, @"(^\s*).*?(\s*)$", "$1|$2").Replace("|", value);
            newDecl += curDeclaration + ":" + newValue + ";";
          }
          // when the action is "r" (remove), do nothing, so the declaration will be removed
        }
      }
      // if we need to add a new property, do so now!
      if (action == "a")
        newDecl += indentation + declaration + ":" + value + ";\n";

      result = selectorText + newDecl + closingAccolade;
      return result;
    }
  }
}